/*
 * vim: ts=4 sw=4 fdm=marker noet tw=78 ai
 * %%LICENSE%%
 */
#include <sys/cdefs.h>
#ifdef __FBSDID
__FBSDID("$Coar$");
#endif
#include <sys/time.h> /* struct timeval */

#include <stdlib.h>
#include <stdint.h>
#include <readpassphrase.h>
#include <assert.h>

#include <err.h>
#include <sysexits.h>

#include <ldap.h>

#include "extern.h"
#include "connect.h"

static int
mk_uri(runtime_t *ctx);

#define SETOPT(__ctx, __opt, __val) \
	ldap_set_option(((__ctx)->ld), __opt, __val)
#define GETOPT(__ctx, __opt, __val) \
	ldap_get_option(((__ctx)->ld), __opt, (__val))

#define PASSWORDLEN 128 /* same as getpass() */

int
lde_connect(runtime_t *ctx)
{
	int res, protv = 3;
	const struct timeval tv = { 30, 0 };
	const int deref = LDAP_DEREF_SEARCHING;

	if( ctx->opt_conn_uri == NULL && ctx->opt_conn_host != NULL )
	{
		if( mk_uri(ctx) != 0 )
		{
			warn("No host or uri information available");
			return LDAP_OPERATIONS_ERROR;
		}
	}
	else
	{
		GETOPT(ctx, LDAP_OPT_URI, &(ctx->opt_conn_uri));
	}
	LOG(ctx, 1, "Connecting to %s", ctx->opt_conn_uri);

	res = ldap_initialize(&(ctx->ld), ctx->opt_conn_uri);

	if( res != LDAP_SUCCESS )
		return (res);

	if( ctx->opt_base != NULL )
	{
		if( (res = SETOPT(ctx, LDAP_OPT_DEFBASE, ctx->opt_base)) !=
				LDAP_SUCCESS )
			return res;
	}
	if( (res = SETOPT(ctx, LDAP_OPT_DEREF, &deref)) )
		warnx("Failed to set dereference option");

	/*
	 * SASL properties
	 * FIXME:
	 *   At present SASL is broken unless you specify things on the command
	 *   line. This is because we have not yet created our interaction
	 *   routine and it's unclear if the ldap library or the SASL library
	 *   fills in the blanks by prompting us.
	 */
	if( ctx->opt_conn_sasl_authcid )
	{
		if( (res = SETOPT(ctx, LDAP_OPT_X_SASL_AUTHCID,
						ctx->opt_conn_sasl_authcid)) != LDAP_SUCCESS )
		{
			warnx("Failed to set SASL authentication id");
			return (res);
		}
	}
	if( ctx->opt_conn_sasl_authzid )
	{
		if( (res = SETOPT(ctx, LDAP_OPT_X_SASL_AUTHZID,
						ctx->opt_conn_sasl_authzid)) != LDAP_SUCCESS )
		{
			warnx("Failed to set SASL authorization id");
			return (res);
		}
	}
	if( ctx->opt_conn_sasl_mech )
	{
		if( (res = SETOPT(ctx, LDAP_OPT_X_SASL_MECH,
						ctx->opt_conn_sasl_mech)) != LDAP_SUCCESS )
		{
			warnx("Failed to set SASL mechanism");
			return (res);
		}
	}
	if( ctx->opt_conn_sasl_realm )
	{
		if( (res = SETOPT(ctx, LDAP_OPT_X_SASL_REALM,
						ctx->opt_conn_sasl_realm)) != LDAP_SUCCESS )
		{
			warnx("Failed to set SASL realm");
			return (res);
		}
	}
	if( ctx->opt_conn_sasl_props )
	{
		if( (res = SETOPT(ctx, LDAP_OPT_X_SASL_SECPROPS,
						ctx->opt_conn_sasl_props)) != LDAP_SUCCESS )
		{
			warnx("Failed to set SASL security properties");
			return (res);
		}
	}

	if( (res = SETOPT(ctx, LDAP_OPT_NETWORK_TIMEOUT, &tv)) != LDAP_SUCCESS )
	{
		warnx("Failed to set timeout");
		return (res);
	}
	if( (res = SETOPT(ctx, LDAP_OPT_PROTOCOL_VERSION, &protv))
			!= LDAP_SUCCESS )
	{
		warnx("Failed to set protocol version");
		return (res);
	}
	/*
	 * All options are set. Time to bind:
	 * We use synchronous binds (and synchronous operations overall) as there
	 * is little benefit to async operation in a run-and-die program.
	 */
	if( ctx->opt_conn_pwprompt )
	{
		ctx->pw.bv_val = malloc(PASSWORDLEN);
		if( readpassphrase("Enter LDAP password:", ctx->pw.bv_val,
					PASSWORDLEN, RPP_REQUIRE_TTY) == NULL )
		{
			err(EX_NOINPUT, "Can't read passphrase");
		}
		ctx->pw.bv_len = strlen(ctx->pw.bv_val);
	}
	else if( ctx->opt_conn_pwfile )
	{
		size_t r;

		ctx->pw.bv_val = malloc(PASSWORDLEN);
		r = fread(ctx->pw.bv_val, 1, ctx->pw.bv_len-1, ctx->opt_conn_pwfile);
		ctx->pw.bv_val[r] = '\0';
		ctx->pw.bv_len = r;
		fclose(ctx->opt_conn_pwfile);
		ctx->opt_conn_pwfile = NULL;
	}

	if( ctx->opt_tls )
	{
		LOG(ctx, 1, "Negotiating TLS connection");
		if( (res = ldap_start_tls_s(ctx->ld, NULL, NULL)) != LDAP_SUCCESS )
		{
			char *msg = NULL;

			GETOPT(ctx, LDAP_OPT_DIAGNOSTIC_MESSAGE, &msg);
			warnx("TLS negotiation failed: %s\n%s", ldap_err2string(res),
					msg);
			ldap_memfree(msg);
			if( ctx->opt_tls > 1 )
				exit(EX_UNAVAILABLE);
		}
	}
	LOG(ctx, 1, "Binding using %s", ctx->opt_conn_dn);
	// We don't support client side controls
	res = ldap_sasl_bind_s(ctx->ld, ctx->opt_conn_dn, ctx->opt_conn_sasl_mech,
			&(ctx->pw), NULL, NULL, &(ctx->scred));

	/*
	 * Zero-out pw so it doesn't linger in process' address space.
	 * This used to give problems with ctx->ld becoming NULL, as it would
	 * overwrite too much of the addresspace (pw.bv_len was undefined).
	 * This is why the assert() is here.
	 */
	if( ctx->pw.bv_len )
	{
		memset(ctx->pw.bv_val, 0, ctx->pw.bv_len);
		ctx->pw.bv_len = 0;
	}
	assert(ctx->ld != NULL);

	return (res);
}

/*
 * Create a URI from host and port parameters.
 * NOTE: Can only be called if ctx->opt_conn_host is non-NULL.
 *
 * Not supported:
 * - socket (ldapi://)
 * - SSL (ldaps://)
 */
static int
mk_uri(runtime_t *ctx)
{
	int res;

	assert(ctx != NULL);
	assert(ctx->opt_conn_host != NULL);
	/*
	 * There's one case left where host is set to the empty string.
	 * We should shoot people who do that, but it's strictly speaking not an
	 * error. I just don't like the behavior - pitfall for "undefined"
	 * results.
	 */
	res = asprintf(&ctx->opt_conn_uri, "ldap://%s:%u/",
			(ctx->opt_conn_host[0]) ? ctx->opt_conn_host : "localhost",
			(ctx->opt_conn_port) ? ctx->opt_conn_port : LDAP_PORT);

	if( res > 0 )
		return (0);
	else
		return res;
}
